Odklenite napredno obdelavo videa v brskalniku. Naučite se neposrednega dostopa in manipulacije surovih podatkov ravnin VideoFrame z API-jem WebCodecs za učinke in analizo po meri.
Dostop do ravnin VideoFrame v WebCodecs: Poglobljen pregled manipulacije surovih video podatkov
Leta so se visoko zmogljiva obdelava videa v spletnem brskalniku zdele kot oddaljene sanje. Razvijalci so bili pogosto omejeni na zmožnosti elementa <video> in 2D Canvas API-ja, ki sta, čeprav zmogljiva, prinašala ozka grla v delovanju in omejen dostop do osnovnih surovih video podatkov. Prihod API-ja WebCodecs je temeljito spremenil to področje, saj zagotavlja nizkonivojski dostop do vgrajenih medijskih kodekov v brskalniku. Ena njegovih najrevolucionarnejših funkcij je zmožnost neposrednega dostopa in manipulacije surovih podatkov posameznih video sličic prek objekta VideoFrame.
Ta članek je celovit vodnik za razvijalce, ki želijo preseči preprosto predvajanje videa. Raziskali bomo podrobnosti dostopa do ravnin VideoFrame, demistificirali koncepte, kot so barvni prostori in razporeditev v pomnilniku, ter ponudili praktične primere, ki vam bodo omogočili izgradnjo naslednje generacije video aplikacij v brskalniku, od filtrov v realnem času do sofisticiranih nalog računalniškega vida.
Predpogoji
Za kar najboljši izkoristek tega vodnika bi morali imeti dobro razumevanje:
- Sodobnega JavaScripta: Vključno z asinhronim programiranjem (
async/await, Promises). - Osnovnih video konceptov: Poznavanje izrazov, kot so sličice, ločljivost in kodeki, je koristno.
- API-jev brskalnika: Izkušnje z API-ji, kot sta Canvas 2D ali WebGL, bodo koristne, vendar niso nujno potrebne.
Razumevanje video sličic, barvnih prostorov in ravnin
Preden se poglobimo v API, si moramo najprej ustvariti trden miselni model o tem, kako podatki video sličice dejansko izgledajo. Digitalni video je zaporedje mirujočih slik oziroma sličic. Vsaka sličica je mreža slikovnih pik (pikslov) in vsaka slikovna pika ima barvo. Kako je ta barva shranjena, določata barvni prostor in format slikovnih pik.
RGBA: Domači jezik spleta
Večina spletnih razvijalcev pozna barvni model RGBA. Vsako slikovno piko predstavljajo štiri komponente: rdeča (Red), zelena (Green), modra (Blue) in alfa (prosojnost). Podatki so običajno shranjeni prepleteno (interleaved) v pomnilniku, kar pomeni, da so vrednosti R, G, B in A za eno slikovno piko shranjene zaporedno:
[R1, G1, B1, A1, R2, G2, B2, A2, ...]
V tem modelu je celotna slika shranjena v enem samem, neprekinjenem bloku pomnilnika. To si lahko predstavljamo kot eno samo "ravnino" podatkov.
YUV: Jezik video kompresije
Video kodeki pa redko delujejo neposredno z RGBA. Raje imajo barvne prostore YUV (ali natančneje, Y'CbCr). Ta model ločuje informacije o sliki na:
- Y (Luma): Informacije o svetlosti ali sivini. Človeško oko je najbolj občutljivo na spremembe v luma.
- U (Cb) in V (Cr): Informacije o krominanci ali barvni razliki. Človeško oko je manj občutljivo na barvne podrobnosti kot na podrobnosti o svetlosti.
Ta ločitev je ključna za učinkovito stiskanje. Z zmanjšanjem ločljivosti komponent U in V – tehnika, imenovana kromatsko podvzorčenje (chroma subsampling) – lahko znatno zmanjšamo velikost datoteke z minimalno zaznavno izgubo kakovosti. To vodi do planarnih formatov slikovnih pik, kjer so komponente Y, U in V shranjene v ločenih pomnilniških blokih ali "ravninah".
Pogost format je I420 (vrsta YUV 4:2:0), kjer so za vsak blok slikovnih pik velikosti 2x2 štirje vzorci Y, a le en vzorec U in en vzorec V. To pomeni, da imata ravnini U in V polovično širino in polovično višino ravnine Y.
Razumevanje te razlike je ključno, saj vam WebCodecs omogoča neposreden dostop do teh ravnin, natanko tako, kot jih zagotovi dekoder.
Objekt VideoFrame: Vaš prehod do slikovnih podatkov
Osrednji del te uganke je objekt VideoFrame. Predstavlja eno samo video sličico in ne vsebuje le slikovnih podatkov, ampak tudi pomembne metapodatke.
Ključne lastnosti objekta VideoFrame
format: Niz, ki označuje format slikovnih pik (npr. 'I420', 'NV12', 'RGBA').codedWidth/codedHeight: Polne dimenzije sličice, kot so shranjene v pomnilniku, vključno z morebitnim dodanim polnilom (padding), ki ga zahteva kodek.displayWidth/displayHeight: Dimenzije, ki naj se uporabijo za prikaz sličice.timestamp: Predstavitveni časovni žig sličice v mikrosekundah.duration: Trajanje sličice v mikrosekundah.
Čarobna metoda: copyTo()
Glavna metoda za dostop do surovih slikovnih podatkov je videoFrame.copyTo(destination, options). Ta asinhrona metoda kopira podatke ravnin sličice v medpomnilnik, ki ga zagotovite.
destination:ArrayBufferali tipizirano polje (kot jeUint8Array), ki je dovolj veliko za shranjevanje podatkov.options: Objekt, ki določa, katere ravnine naj se kopirajo in kakšna je njihova razporeditev v pomnilniku. Če je izpuščen, kopira vse ravnine v en sam neprekinjen medpomnilnik.
Metoda vrne obljubo (Promise), ki se razreši s poljem objektov PlaneLayout, po en za vsako ravnino v sličici. Vsak objekt PlaneLayout vsebuje dva ključna podatka:
offset: Odmik v bajtih, kjer se začnejo podatki te ravnine znotraj ciljnega medpomnilnika.stride: Število bajtov med začetkom ene vrste slikovnih pik in začetkom naslednje vrste za to ravnino.
Ključen koncept: Korak (Stride) proti širini
To je eden najpogostejših virov zmede za razvijalce, ki so novi v nizkonivojskem grafičnem programiranju. Ne morete predpostaviti, da so vrste slikovnih podatkov tesno zapakirane ena za drugo.
- Širina (Width) je število slikovnih pik v eni vrsti slike.
- Korak (Stride) (imenovan tudi pitch ali line step) je število bajtov v pomnilniku od začetka ene vrste do začetka naslednje.
Pogosto bo stride večji od širina * bajtov_na_piko. To je zato, ker je pomnilnik pogosto dopolnjen s polnilom za poravnavo z mejami strojne opreme (npr. 32- ali 64-bajtnimi mejami) za hitrejšo obdelavo s strani CPE ali GPE. Za izračun pomnilniškega naslova slikovne pike v določeni vrsti morate vedno uporabiti korak (stride).
Ignoriranje koraka bo vodilo do poševnih ali popačenih slik in nepravilnega dostopa do podatkov.
Praktični primer 1: Dostop in prikaz sivinske ravnine
Začnimo s preprostim, a zmogljivim primerom. Večina videa na spletu je kodirana v formatu YUV, kot je I420. Ravnina 'Y' je dejansko popolna sivinska predstavitev slike. Lahko izvlečemo samo to ravnino in jo izrišemo na platno (canvas).
async function displayGrayscale(videoFrame) {
// Predpostavljamo, da je videoFrame v formatu YUV, kot je 'I420' ali 'NV12'.
if (!videoFrame.format.startsWith('I4')) {
console.error('Ta primer zahteva planarni format YUV 4:2:0.');
videoFrame.close();
return;
}
const yPlaneInfo = videoFrame.layout[0]; // Ravnina Y je vedno prva.
// Ustvarimo medpomnilnik, ki bo vseboval samo podatke ravnine Y.
const yPlaneData = new Uint8Array(yPlaneInfo.stride * videoFrame.codedHeight);
// Kopiramo ravnino Y v naš medpomnilnik.
await videoFrame.copyTo(yPlaneData, {
rect: { x: 0, y: 0, width: videoFrame.codedWidth, height: videoFrame.codedHeight },
layout: [yPlaneInfo]
});
// Sedaj yPlaneData vsebuje surove sivinske slikovne pike.
// To moramo izrisati. Ustvarili bomo medpomnilnik RGBA za platno.
const canvas = document.getElementById('my-canvas');
canvas.width = videoFrame.displayWidth;
canvas.height = videoFrame.displayHeight;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(canvas.width, canvas.height);
// Iteriramo čez slikovne pike platna in jih zapolnimo s podatki iz ravnine Y.
for (let y = 0; y < videoFrame.displayHeight; y++) {
for (let x = 0; x < videoFrame.displayWidth; x++) {
// Pomembno: Uporabite korak (stride) za iskanje pravilnega izvornega indeksa!
const yIndex = y * yPlaneInfo.stride + x;
const luma = yPlaneData[yIndex];
// Izračunamo ciljni indeks v medpomnilniku RGBA ImageData.
const rgbaIndex = (y * canvas.width + x) * 4;
imageData.data[rgbaIndex] = luma; // Rdeča
imageData.data[rgbaIndex + 1] = luma; // Zelena
imageData.data[rgbaIndex + 2] = luma; // Modra
imageData.data[rgbaIndex + 3] = 255; // Alfa
}
}
ctx.putImageData(imageData, 0, 0);
// KRITIČNO: Vedno zaprite VideoFrame, da sprostite njegov pomnilnik.
videoFrame.close();
}
Ta primer poudarja več ključnih korakov: določitev pravilne postavitve ravnine, alokacija ciljnega medpomnilnika, uporaba copyTo za pridobitev podatkov in pravilno iteriranje čez podatke z uporabo stride za sestavo nove slike.
Praktični primer 2: Manipulacija na mestu (filter Sepia)
Sedaj pa izvedimo neposredno manipulacijo podatkov. Filter sepia je klasičen učinek, ki ga je enostavno implementirati. Za ta primer je lažje delati s sličico RGBA, ki jo lahko dobite s platna ali iz konteksta WebGL.
async function applySepiaFilter(videoFrame) {
// Ta primer predpostavlja, da je vhodna sličica v formatu 'RGBA' ali 'BGRA'.
if (videoFrame.format !== 'RGBA' && videoFrame.format !== 'BGRA') {
console.error('Primer s filtrom sepia zahteva sličico RGBA.');
videoFrame.close();
return null;
}
// Alociramo medpomnilnik za shranjevanje slikovnih podatkov.
const frameDataSize = videoFrame.allocationSize();
const frameData = new Uint8Array(frameDataSize);
await videoFrame.copyTo(frameData);
const layout = videoFrame.layout[0]; // RGBA je ena sama ravnina
// Sedaj manipuliramo s podatki v medpomnilniku.
for (let y = 0; y < videoFrame.codedHeight; y++) {
for (let x = 0; x < videoFrame.codedWidth; x++) {
const pixelIndex = y * layout.stride + x * 4; // 4 bajti na slikovno piko (R,G,B,A)
const r = frameData[pixelIndex];
const g = frameData[pixelIndex + 1];
const b = frameData[pixelIndex + 2];
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
frameData[pixelIndex] = Math.min(255, tr);
frameData[pixelIndex + 1] = Math.min(255, tg);
frameData[pixelIndex + 2] = Math.min(255, tb);
// Alfa (frameData[pixelIndex + 3]) ostane nespremenjena.
}
}
// Ustvarimo *novo* sličico VideoFrame s spremenjenimi podatki.
const newFrame = new VideoFrame(frameData, {
format: videoFrame.format,
codedWidth: videoFrame.codedWidth,
codedHeight: videoFrame.codedHeight,
timestamp: videoFrame.timestamp,
duration: videoFrame.duration
});
// Ne pozabite zapreti originalne sličice!
videoFrame.close();
return newFrame;
}
To prikazuje celoten cikel branja-spreminjanja-pisanja: kopiraj podatke ven, z zanko jih preglej z uporabo koraka, uporabi matematično transformacijo na vsaki slikovni piki in sestavi nov VideoFrame z rezultirajočimi podatki. To novo sličico lahko nato izrišemo na platno, pošljemo v VideoEncoder ali predamo naslednjemu koraku obdelave.
Zmogljivost je pomembna: JavaScript proti WebAssembly (WASM)
Iteriranje čez milijone slikovnih pik za vsako sličico (sličica 1080p ima več kot 2 milijona slikovnih pik ali 8 milijonov podatkovnih točk v RGBA) v JavaScriptu je lahko počasno. Čeprav so sodobni JS pogoni izjemno hitri, lahko pri obdelavi videa visoke ločljivosti (HD, 4K) v realnem času ta pristop zlahka preobremeni glavno nit, kar vodi do zatikajoče uporabniške izkušnje.
Tu postane WebAssembly (WASM) bistveno orodje. WASM vam omogoča zagon kode, napisane v jezikih, kot so C++, Rust ali Go, s skoraj naravno hitrostjo znotraj brskalnika. Delovni potek za obdelavo videa postane:
- V JavaScriptu: Uporabite
videoFrame.copyTo(), da dobite surove slikovne podatke vArrayBuffer. - Predajte v WASM: Referenco na ta medpomnilnik predajte v svoj preveden modul WASM. To je zelo hitra operacija, saj ne vključuje kopiranja podatkov.
- V WASM (C++/Rust): Izvedite svoje visoko optimizirane algoritme za obdelavo slik neposredno na pomnilniškem medpomnilniku. To je za več redov velikosti hitreje kot zanka v JavaScriptu.
- Vrnitev v JavaScript: Ko WASM konča, se nadzor vrne v JavaScript. Nato lahko uporabite spremenjeni medpomnilnik za ustvarjanje novega objekta
VideoFrame.
Za katero koli resno aplikacijo za manipulacijo videa v realnem času – kot so virtualna ozadja, zaznavanje predmetov ali kompleksni filtri – uporaba WebAssembly ni le možnost; je nuja.
Obravnavanje različnih slikovnih formatov (npr. I420, NV12)
Čeprav je RGBA preprost, boste najpogosteje prejemali sličice v planarnih formatih YUV iz VideoDecoder. Poglejmo si, kako obravnavati popolnoma planarni format, kot je I420.
VideoFrame v formatu I420 bo imel v svojem polju layout tri deskriptorje postavitve:
layout[0]: Ravnina Y (luma). Dimenzije socodedWidthxcodedHeight.layout[1]: Ravnina U (kroma). Dimenzije socodedWidth/2xcodedHeight/2.layout[2]: Ravnina V (kroma). Dimenzije socodedWidth/2xcodedHeight/2.
Takole bi kopirali vse tri ravnine v en sam medpomnilnik:
async function extractI420Planes(videoFrame) {
const totalSize = videoFrame.allocationSize({ format: 'I420' });
const allPlanesData = new Uint8Array(totalSize);
const layouts = await videoFrame.copyTo(allPlanesData);
// layouts je polje treh objektov PlaneLayout
console.log('Postavitev ravnine Y:', layouts[0]); // { offset: 0, stride: ... }
console.log('Postavitev ravnine U:', layouts[1]); // { offset: ..., stride: ... }
console.log('Postavitev ravnine V:', layouts[2]); // { offset: ..., stride: ... }
// Sedaj lahko dostopate do vsake ravnine znotraj medpomnilnika `allPlanesData`
// z uporabo njenega specifičnega odmika in koraka.
const yPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[0].offset,
layouts[0].stride * videoFrame.codedHeight
);
// Upoštevajte, da so dimenzije krome prepolovljene!
const uPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[1].offset,
layouts[1].stride * (videoFrame.codedHeight / 2)
);
const vPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[2].offset,
layouts[2].stride * (videoFrame.codedHeight / 2)
);
console.log('Velikost dostopane ravnine Y:', yPlaneView.byteLength);
console.log('Velikost dostopane ravnine U:', uPlaneView.byteLength);
videoFrame.close();
}
Drug pogost format je NV12, ki je pol-planaren. Ima dve ravnini: eno za Y in drugo ravnino, kjer so vrednosti U in V prepletene (npr. [U1, V1, U2, V2, ...]). API WebCodecs to obravnava transparentno; VideoFrame v formatu NV12 bo preprosto imel dve postavitvi v svojem polju layout.
Izzivi in najboljše prakse
Delo na tako nizki ravni je močno, vendar prinaša odgovornosti.
Upravljanje pomnilnika je ključnega pomena
VideoFrame zadržuje znatno količino pomnilnika, ki se pogosto upravlja zunaj kupa zbiralnika smeti (garbage collector) JavaScripta. Če tega pomnilnika izrecno ne sprostite, boste povzročili uhajanje pomnilnika (memory leak), ki lahko sesuje zavihek brskalnika.
Vedno, ampak res vedno, pokličite videoFrame.close(), ko končate z uporabo sličice.
Asinhrona narava
Ves dostop do podatkov je asinhron. Arhitektura vaše aplikacije mora pravilno obravnavati tok obljub (Promises) in async/await, da se izognete tekmovalnim stanjem (race conditions) in zagotovite nemoten cevovod obdelave.
Združljivost z brskalniki
WebCodecs je sodoben API. Čeprav je podprt v vseh večjih brskalnikih, vedno preverite njegovo razpoložljivost in se zavedajte morebitnih specifičnih podrobnosti ali omejitev implementacije posameznega ponudnika. Pred poskusom uporabe API-ja uporabite zaznavanje funkcij.
Zaključek: Nova meja za spletni video
Zmožnost neposrednega dostopa in manipulacije surovih podatkov ravnin VideoFrame prek API-ja WebCodecs je premik paradigme za spletne medijske aplikacije. Odstranjuje črno skrinjico elementa <video> in daje razvijalcem natančen nadzor, ki je bil prej rezerviran za nativne aplikacije.
Z razumevanjem osnov razporeditve video pomnilnika – ravnin, koraka in barvnih formatov – ter z izkoriščanjem moči WebAssembly za operacije, ki so kritične za zmogljivost, lahko zdaj gradite izjemno sofisticirana orodja za obdelavo videa neposredno v brskalniku. Od barvne korekcije v realnem času in vizualnih učinkov po meri do strojnega učenja na strani odjemalca in video analize, možnosti so ogromne. Obdobje visoko zmogljivega, nizkonivojskega videa na spletu se je zares začelo.